查看原文
其他

捋一捋,到底怎么样去理解Window机制?

d袋鼠b 技术最TOP 2022-08-26

作者:d袋鼠b, 链接:https://juejin.cn/post/6952341892721803294

说到 Window 机制,通常想到的就是PhoneWindowViewRootImplWindowManagerImpl子窗口DecorView 等等,网上也有不少博客通过源码分析他们之间的调用关系,可是能说得比较清楚的却不多,或深入源码不可自拔,或越说越复杂概念一大堆。

今天,我们就来好好捋一捋,到底怎么样去理解Window机制呢?

先撇开复杂源码与难懂的概念,我们现在谈的Window都是单纯地指Window这个类,请读者暂时抛开"窗口"、“子窗口”、 “子Window” 这些扰乱思路的概念,文章后面也会对这些概念以及应用作出说明和解释,所以先看看Window这个类,由简单到复杂。(" 子Window" 可以视为一个错误的说法,这词在对理解Window机制上没什么好处,所以请永远忘记它!!!) 为了有更好的阅读思路,这里先给出几条结论:

  • 每个Window都有自己的唯一的一个WindowManagerWindowManager负责维护它所在的Window里面的内容。
  • 在Android平台上Window的具体实现是PhoneWindow、对应它的WindowManagerWindowManagerImpl
  • WindowManagerImpl维护PhoneWindow中的什么内容呢? 答案:DecorView及其子View所组成的View树。
  • 一个Activity有且只有一个PhoneWindow实例,一个Dialog同样有且只有一个PhoneWindow实例。

可能聪明的同学就会问了: 怎么没看到你的理解中有ViewRootImpl、WindowManagerGlobal这些呢?我给张图你就明白了。

如上图:

  • 一个Window都有一个WindowManager负责管理它的内容。

  • PhoneWindow的内容则交给WindowManagerImpl管理,其中的“内容”指的则是DecorView及其子View所组成的View树。

  • WindowManagerImpl决定管理View树时,会给这个View树分配一个ViewRootImpl,ViewRootImpl则是负责与其他服务联络并指导View树的绘制工作。

  • ActivityPhoneWindow对象,Dialog同样也有自己的PhoneWindow对象

Window与WindowManager

虽然在 Android 平台上PhoneWindowWindow的唯一实现,可是直接跳过Window直接看PhoneWindow这种做法并不值得提倡。

打开Window的源码,会发现它有一个WindowManager对象。这个WindowManager就负责管理当前Window对象的内容。

//: Window.java

public abstract class Window {
    ...
    private WindowManager mWindowManager;
    ...
    
    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) 
{
        mAppToken = appToken;
        mAppName = appName;
        mHardwareAccelerated = hardwareAccelerated;
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
}

所以,不管Window的实现类是不是PhoneWindow,它都会有一个它自己的管理者WindowManager管理它的内部。

Activity 与 PhoneWindow

接下来再看看Window的唯一实现PhoneWindow。在 Android 中脱离ActivityPhoneWindow都是耍流氓。

所以这里将主要介绍Actitity启动过程中,PhoneWinow的创建流程、WindowManagerImpl的绑定、DecorView的创建以及ViewRootImpl的分配。


创建PhoneWindow

首先一起看一下PhoneWindow是何时创建的。众所周知,Activity 启动时 ActivityThread会调用 performLaunchActivity()方法创建一个Activity实例,紧接着会调用它的 attach 方法。

//: ActivityThread.java

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
         Activity activity = null;
         ...
         java.lang.ClassLoader cl = appContext.getClassLoader();
         activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
         Application app = r.packageInfo.makeApplication(false, mInstrumentation);
         ...
         activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback,
                        r.assistToken);
    }

Activityattach 方法当中,PhoneWindow被创建并赋值给 Activity 的成员变量mWindow,紧接着会为PhoneWindow设置一个WindowManager

//: Activity.java

    final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) 
{
        ...
        //创建PhoneWindow对象
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
        //为PhoneWindow设置WindowManager
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
        
        mWindowManager = mWindow.getWindowManager();
    }
//: Window.java

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
            boolean hardwareAccelerated) 
{
        ...
        if (wm == null) {
            wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
        }
        //创建WindowManagerImpl实例,并赋值给Window的成员变量mWindowManager
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }

//: WindowManagerImpl.java

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
        return new WindowManagerImpl(mContext, parentWindow);
    }

可以看到Activity并不是天生就有PhoneWindow的,只是在启动过程中Activityattch()方法被调用,然后才会给Activity创建PhoneWindow对象,并在PhoneWindow创建后紧接着给它新建一个WindowManagerImpl

attch()完成后,Activity 就是这个样子了。

创建DecorView

PhoneWindow及其内容管理者WindowManagerImpl创建好了以后,就需要关心管理者所管理的View树是何时创建了。也就是DecorView何时创建,这一点大部分读者都比较熟悉,在 Activity 中调用 setContentView()时,DecorView会被创建。

//: Activity.java

    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

上述的 getWindow().setContentView(layoutResID)实际调用的就是PhoneWindowsetContentView方法。其源码如下:

//: PhoneWondow.java

    @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            //创建DecorView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }
        ...
    }
    
    private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            //创建DecorView并赋值给PhoneWindow的成员变量mDecor
            mDecor = generateDecor(-1);
            ...
        }
        ...
    }
    
    protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, this);
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        //新建DecorView对象,并将PhoneWindow实例传入其中
        return new DecorView(context, featureId, this, getAttributes());
    }

完成setContent后,PhoneWindow也有自己的DecorView了。不过到目前为止,虽然ActivityPhoneWindow,有PhoneWindow也有WindowManagerImplDecorView(或View树)了,此时的 Activity仍然不会在界面上显示任何东西。

要在界面上显示View树,必须要通知WindowManagerServices才可以,这个通知工作在是在ViewRootImpl之中完成,然而目前还未曾创建ViewRootImpl

创建ViewRootImpl

上面讲到在View树显示到界面之前,需要有一个ViewRootImpl负责指导它的绘制显示工作。那这个ViewRootImpl是何时创建并与View树关联的呢?

一句话简要概况:当WindowManagerImpl决定管理View树时,会给它关联一个ViewRootImpl实例。

从代码层面来说就是WindowManagerImpl调用addView方法将View树添加到View列表中时。

先看一下WindowManagerImpladdView方法:

//: WindowManagerImpl.java

    //单例 WindowManagerGlobal
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    
    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

可以看到所有的WindowManagerImpl对View的管理都是交给一个唯一的WindowManagerGlobal了。所以,我们需要再看看WindowManagerGlobaladdView方法:

//: WindowManagerGlobal.java
    private static IWindowSession sWindowSession;
    
    private final ArrayList<View> mViews = new ArrayList<View>();
    @UnsupportedAppUsage
    private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
    @UnsupportedAppUsage
    private final ArrayList<WindowManager.LayoutParams> mParams =
            new ArrayList<WindowManager.LayoutParams>();
            
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) 
{
        ...
        final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
        ...
        ViewRootImpl root;
        View panelParentView = null;

        synchronized (mLock) {
            ...
            // 创建ViewRootImpl实例
            root = new ViewRootImpl(view.getContext(), display);
            // 给View设置属性
            view.setLayoutParams(wparams);
            
            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
            ...
            //将ViewRootImp于View进行绑定
            root.setView(view, wparams, panelParentView);
            ...
        }
    }

可以看到当WindowManagerGlobaladdView方法被调用时,ViewRootImpl被创建并将其与View树进行绑定。ViewRootImpl与View树进行绑定后,Activity看起来就是这个样子了。

看到这里又有同学要举手提问了,这个Activity何时会让WindowManagerImpl执行addView操作呢?

为了解决这个问题?我们需要再回到Activity的启动流程,观察ActivityThreadhandleResumeActivity方法:

//: ActivityThread.java

    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) 
{
        ...
        // TODO Push resumeArgs into the activity for consideration
        //内部会调用Activity的onResume方法
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
        
        final Activity a = r.activity;

        final int forwardBit = isForward
                ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0;

        // If the window hasn't yet been added to the window manager,
        // and this guy didn't finish itself or start another activity,
        // then go ahead and add the window.
        boolean willBeVisible = !a.mStartedActivity;
        if (!willBeVisible) {
            try {
                willBeVisible = ActivityTaskManager.getService().willActivityBeVisible(
                        a.getActivityToken());
            } catch (RemoteException e) {
                throw e.rethrowFromSystemServer();
            }
        }
        //从willBeVisible的注释中可以了解到当窗口没有被添加到WindowManager中时willBeVisible为true
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            //窗口类型设置
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
           
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //WindowManagerImpl将View添加到自己的管理队列中
                    wm.addView(decor, l);
                } else {
                    a.onWindowAttributesChanged(l);
                }
            }

        }
    }

阅读完handleResumeActivity方法后,WindowManagerImpladdView方法正式在此方法中完成的。

流程有点长,先小结一下之前的内容:

  • 第一步:在Activity的启动过程中,ActivityThread会创建一个Activity实例。

  • 第二步:在Activity实例创建完成后,紧接着执行Activity#attach()方法。

  • 第三步:在Activity#attach()方法之内PhoneWindow被创建,并同时创建一个WindowManagerImpl负责维护PhoneWindow内的内容。

  • 第四部:在Activity#onCreate()中调用setContent()方法,这个方法内部创建一个DecorView实例作为PhoneWindow的内容。

  • 第五步:在ActivityThread#handleResumeActivity()方法中WindowManagerImpl#addView被调用,WindowManagerImpl决定管理DecorView,并创建一个ViewRootImpl实例,将ViewRootImpl与View树进行关联,这样ViewRootImpl就可以指挥View树的具体工作。

通过后面的学习可以了解到,ViewRootImpl指挥View树的这些工作包含:View树的显示、测量绘制、同步刷新以及事件分发。

惊不惊喜,意不意外???ViewRootImpl原来这么重要,传说的众View之父呀!

仔细想一下,就知道其实没什么意外的~~~

ViewRootImpl与View

刚刚我们交代了ViewRootImpl的创建时机,同时也讲到View树的显示、测量绘制、同步刷新以及事件分发这些工作与ViewRootImpl联系紧密。

ViewRootImpl再看看它的具体实现:

//: ViewRootImpl.java

    final IWindowSession mWindowSession;
    final W mWindow;
    Choreographer mChoreographer;
 
    WindowInputEventReceiver mInputEventReceiver;
    
    public ViewRootImpl(Context context, Display display) {
        ...
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mWindow = new W(this);
        mChoreographer = Choreographer.getInstance();
        ...
    }
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
                // Schedule the first layout -before- adding to the window
                // manager, to make sure we do the relayout before receiving
                // any other events from the system.
                //WMS添加窗口之前,进行一次测量布局工作,这样才能保证各类系统事件与View位置对应关系的准确性
                requestLayout();
                //通过mWindowSession通知WMS添加并显示窗口
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                            mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                            mTempInsets);
              ......
              //初始化事件接收器,
              mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
              //将当前ViewRootImpl实例赋值给DecorView的mParent变量
              //众View之父,由此而来
              view.assignParent(this);
  }
                            

ViewRootImpl中有mIWindowSessionmWindowmChoreographermInputEventReceiver三个比较重要的变量。

mIWindowSession与mWindow

mIWindowSession、mWindow为Binder对象,用于APP端与WMS之间的相互通信。细心的读者可能会发现,在ViewRootImpl对象创建后紧接着有一个ViewRootImplsetView操作。不难发现,在ViewRootImpl#setView中使用了IWindowSession.addToDisplay来通知WMS添加并显示窗口。对WMS添加窗口细节感兴趣的读者可以参看《WindowManagerService窗口管理之Window添加流程》

关于APP端使用mIWindowSessionmWindowWMS通讯细节的问题,可以参考文章: https://blog.csdn.net/yangwen123/article/details/18733631

mChoreographer

对于部分同学来说Choreographer可能并不陌生,我们通常叫做“编舞者”。无论是系统同步刷新、View的requestLayout还是界面帧率监控,都能看到Choreographer的身影。

Choreographer要是展开讲的话,内容太长。希望读者可以去看看袁辉辉大佬的《Choreographer原理》,或者也可以看看这篇《Android 怎么就不卡了呢之Choreographer》或者《你真的了解16.6ms刷新机制吗?》

mInputEventReceiver

最后一点要讲的就是WindowInputEventReceiver在事件分发事件中的作用。可能一谈到事件分发,部分同学就会想到View的事件分发,可是View的事件是谁传给它的呢?由于笔者水平有限,强烈推荐袁辉辉大佬的《Input系统—事件处理全过程》,如果觉得难度太大的话可以先看看《原来Android触控机制竟是这样的?》。

子窗口

一说到Window机制,不少同学就喜欢聊子窗口,可是我却不喜欢聊它。为啥?在我看来这个概念太务虚了,就像去了解“茴香豆”的“茴”字有几种写法是一个道理,咬文嚼字不值得提倡。另外一点,它干扰了我们理解Window机制。

首先,什么是窗口?警告:一个窗口本质上是一个View树,就像Activity中DecorView及其子View组成的一颗View树,就可以视作是Activity内的一个窗口,注意它并不是Window对象。

之前我们讲到Activity启动过程中,会创建一个PhoneWindow与对应的WindowManagerImpl对象,然后在setContent的时候会给PhoneWindow创建一个DecorView,最终在WindowManagerImpl#addView中给DecorView分配一个ViewRootImpl对象负责指导DecorView的工作。这个DecorView及其子View组成的View树就是一个窗口。它的窗口类型为WindowManager.LayoutParams.TYPE_BASE_APPLICATION(在ActivityThread#handleResumeActivity()方法中被设置的),所以我们把它叫做应用窗口。

那什么是子窗口?

在PhoneWindow与WindowManagerImpl创建好了以后,我们自己也可以调用WindowManagerImpl#addView来添加一个View树,也叫添加窗口。当它的窗口类型处于WindowManager.LayoutParams.FIRST_SUB_WINDOWWindowManager.LayoutParams.LAST_SUB_WINDOW之间时,我们称这个直接添加的窗口为子窗口。

当然,也有的同学把直接调用WindowManagerImpl#addView创建的窗口都叫做子窗口。本文并未采取这种方式定义子窗口这一概念。

那这些直接通过WindowManagerImpl#addView创建的窗口公用同一个PhoneWindow以及WindowManagerImpl对象,窗口类型不同决定了它们在WMS上的一个z-order顺序。

PopupWindow

接下来让我们看看子窗口是如何添加的,以PopupWindow为例:

//: PopupWindow.java

private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;

    public void showAtLocation(View parent, int gravity, int x, int y) {
        mParentRootView = new WeakReference<>(parent.getRootView());
        //这个getWindowToken返回的就是ViewRootImpl里的mWindow对象
        showAtLocation(parent.getWindowToken(), gravity, x, y);
    }
    
        public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        detachFromAnchor();

        mIsShowing = true;
        mIsDropdown = false;
        mGravity = gravity;
        //创建窗口属性,配置窗口类型
        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
        preparePopup(p);

        p.x = x;
        p.y = y;

        invokePopup(p);
    }
    
    protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
        p.gravity = computeGravity();
        p.flags = computeFlags(p.flags);
        p.type = mWindowLayoutType;
        p.token = token;
        p.softInputMode = mSoftInputMode;
        p.windowAnimations = computeAnimationResource();

        return p;
    }
    //添加窗口
    private void invokePopup(WindowManager.LayoutParams p) {
        if (mContext != null) {
            p.packageName = mContext.getPackageName();
        }

        final PopupDecorView decorView = mDecorView;
        decorView.setFitsSystemWindows(mLayoutInsetDecor);

        setLayoutDirectionFromAnchor();
        //添加窗口
        mWindowManager.addView(decorView, p);

        if (mEnterTransition != null) {
            decorView.requestEnterTransition(mEnterTransition);
        }
    }  

可以看到PopupWindow通过WindowManagerImpl直接添加了一个类型为WindowManager.LayoutParams.TYPE_APPLICATION_PANEL类型的窗口。

这个mWindowManager就是ActivityWindowManager,也就是Activity内PhoneWindowWindowManagerImpl,所以,这个窗口要依附于Activity。Application没有WindowManager所以不能被依附。

另一方面,这个窗口类型恰好是一个FIRST_SUB_WINDOW类型,所以PopupWindow是一个真正的子窗口。

//: WindowManager.java
public static final int TYPE_APPLICATION_PANEL = FIRST_SUB_WINDOW;`

Dialog

看完了PopupWindow再来看看Dialog有何不同?

//: Dialog.java

    Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //创建PhoneWindow
        final Window w = new PhoneWindow(mContext);
        mWindow = w;
        w.setCallback(this);
        w.setOnWindowDismissedCallback(this);
        w.setOnWindowSwipeDismissedCallback(() -> {
            if (mCancelable) {
                cancel();
            }
        });
        w.setWindowManager(mWindowManager, nullnull);
        w.setGravity(Gravity.CENTER);

        mListenersHandler = new ListenersHandler(this);
    }
    
    public void show() {
        if (mShowing) {
            if (mDecor != null) {
                if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                    mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                }
                mDecor.setVisibility(View.VISIBLE);
            }
            return;
        }
        ......
        onStart();
        mDecor = mWindow.getDecorView();

        WindowManager.LayoutParams l = mWindow.getAttributes();
        //添加窗口
        mWindowManager.addView(mDecor, l);
    }

可以看到,Dialog与PopupWindow不同,它有自己的PhoneWindow对象,同时Dialog的窗口类型为TYPE_APPLICATION,所以不能视为一个子窗口。

总结

文章篇幅较长,最后我们再总结一下。

无论是Activity还是PopupWindow,又或者Dialog。这些对象需要显示到界面,都是通过调用WindowManagerImpl#addView来间接完成,它们所显示的内容实际就是View树,这个View树我们称之为“窗口”。addView时,会生成一个ViewRootImpl与View树进行关联,ViewRootImpl内部则通过WindowSession来与WMS进行通讯最终完成显示。另一方面,WMS通过一个ViewRootImpl$W实例(mWindow)代理,将WMS端的事件传递到ViewRootImpl,最终交给View树。

参考
  • Choreographer原理
  • 原来Android触控机制竟是这样的?
  • Input系统—事件处理全过程
  • Android 怎么就不卡了呢之Choreographer
  • Android 应用程序建立与WMS服务之间的通信过程
  • Android事件分发机制前篇——事件如何传递到Activity中
  • 你真的了解16.6ms刷新机制吗?


---END---


推荐阅读:
Compose 1.0 即将发布,你准备好了吗?
Google 正式向用户推送 Fuchsia OS
刚刚,鸿蒙来了!上百款设备将陆续启动升级,升级设备清单公布!
Android 仿 Telegram 一样的上传文件炫酷动画!
ConstraintLayout2.0之MotionEffect,简单的代码实现炫酷的动效!
Kotlin 1.5 稳定版发布,2021年第一个大版本更新有何亮点?

更文不易,点个“在看”支持一下👇

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存